Skip to content

feat(auth): implement closed-by-default API key auth, scoped service … - #482

Open
Unclebaffa wants to merge 3 commits into
Bitcoindefi:mainfrom
Unclebaffa:feat/api-key-management
Open

feat(auth): implement closed-by-default API key auth, scoped service …#482
Unclebaffa wants to merge 3 commits into
Bitcoindefi:mainfrom
Unclebaffa:feat/api-key-management

Conversation

@Unclebaffa

@Unclebaffa Unclebaffa commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

🔒 feat(auth): Closed-by-Default API Key Authentication, Scoped Service Keys & Edge Rate Limiting

📋 Summary

This PR establishes an end-to-end, zero-trust security perimeter across Open-Stellar. It transitions the application from an open perimeter to a strict closed-by-default architecture, protecting all /admin/* interfaces, /api/admin/* endpoints, and state-mutating API operations against unauthenticated access and brute-force abuse.

The implementation features cryptographic CSPRNG key generation, SHA-256 zero-knowledge hashing, timing-safe equality checks, granular scope-based access control, sliding-window rate limiting across four tiers, and a dedicated retro-cyberpunk Key Management UI with one-time secret disclosure.


🎯 Key Objectives & Problem Statement

  • Security Gap Closed: All API routes and administrative consoles were previously open to unauthenticated public access.
  • Closed-by-Default Routing: Enforces route protection at the Next.js Edge Middleware layer. Any route not explicitly enumerated on the public allowlist requires valid authentication.
  • Credential Protection (Admin console: display real ADMIN_API_KEY from server (not client-generated demo key) #225): Plaintext secrets are never stored in memory, logs, or databases. The system only stores SHA-256 hashes and masked prefixes (osk_live_abc12...).
  • Edge Runtime Compatibility: Built entirely with standard Web Crypto APIs (globalThis.crypto.subtle), avoiding Node-specific runtime modules in middleware.

🏗 Architecture & Technical Specifications

                     ┌────────────────────────────────────────────────────────┐
                     │              Incoming HTTP Request                      │
                     └──────────────────────────┬─────────────────────────────┘
                                                │
                                                ▼
                     ┌────────────────────────────────────────────────────────┐
                     │          Next.js Edge Proxy Middleware                 │
                     │  - Closed-by-default route evaluator                   │
                     │  - Header & query token extractor                      │
                     └──────────────────────────┬─────────────────────────────┘
                                                │
                 ┌──────────────────────────────┴──────────────────────────────┐
                 │                                                             │
                 ▼                                                             ▼
  [ Public Allowlist Route ]                                    [ Protected Route / Mutation ]
  (e.g., GET /, GET /api/prices)                                               │
                 │                                                             ▼
                 │                                              ┌─────────────────────────────┐
                 │                                              │    Extract API Key Token    │
                 │                                              │ (Bearer osk_live_... / URL) │
                 │                                              └──────────────┬──────────────┘
                 │                                                             │
                 │                                                             ▼
                 │                                              ┌─────────────────────────────┐
                 │                                              │      Constant-Time SHA-256  │
                 │                                              │      Hash Verification      │
                 │                                              └──────────────┬──────────────┘
                 │                                                             │
                 │                             ┌───────────────────────────────┴───────────────────────────────┐
                 │                             │                                                               │
                 │                             ▼                                                               ▼
                 │                     [ Invalid / Expired ]                                           [ Valid Key ]
                 │                             │                                                               │
                 │                             ▼                                                               ▼
                 │                     401 / 403 Response                                      ┌─────────────────────────────┐
                 │                                                                             │  Sliding-Window Rate Limit  │
                 │                                                                             │ (No-Key: 10, Free: 60,      │
                 │                                                                             │  Pro: 600, Admin: ∞)        │
                 │                                                                             └──────────────┬──────────────┘
                 │                                                                                            │
                 │                                                                                            ▼
                 └─────────────────────────────────────┬──────────────────────────────────────────────────────┘
                                                       │
                                                       ▼
                                     ┌───────────────────────────────────┐
                                     │     Route Handler Execution       │
                                     └───────────────────────────────────┘

1. Cryptographic Key Lifecycle (lib/auth/api-keys.ts)

  • Key Format: osk_live_<48-hex-chars> (generated via CSPRNG globalThis.crypto.getRandomValues).
  • Storage Security: Only SHA-256(secret) is stored. Keys are looked up by computing the SHA-256 hash of incoming tokens and comparing digests using timingSafeEqual to prevent side-channel timing attacks.
  • Key Prefixing: Sanitized listing APIs only expose keyPrefix (e.g. osk_live_40e1b...) for administrative identification.
  • Rotation & Instant Revocation: Revoked keys are marked with revokedAt timestamps and cannot be revived. Rotation atomically revokes the previous key and provisions a fresh key.

2. Edge-Compatible Middleware & Route Protection (lib/auth/middleware.ts, middleware.ts)

  • Closed-by-Default Policy:
    • All /admin/* routes require Admin privileges (ADMIN_API_KEY or admin:* scope).
    • All /api/admin/* routes require Admin privileges.
    • State-mutating methods (POST, PUT, PATCH, DELETE) on /api/agents/*, /api/webhooks/*, /api/quests/*, etc., require corresponding write scopes (agents:write, webhooks:write, quests:write).
    • Read-only endpoints on public resources (GET /api/prices, GET /api/openapi.json, static assets) remain accessible to anonymous callers subject to baseline tier rate limiting.
  • Token Ingestion:
    • Authorization: Bearer osk_live_...
    • ?apiKey=osk_live_... (for webhooks and embedded agent integrations).

3. Tiered Sliding-Window Rate Limiting

Tier Limit Scope / Use Case
No key 10 req/min Anonymous public reads
Free 60 req/min Community & hobbyist integrations
Pro 600 req/min Production services, payment settling, webhooks
Admin Unlimited System automation & administrative control

Enforces 429 Too Many Requests with Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers.

4. Admin Credentials Management UI (app/admin/keys/page.tsx, components/admin/admin-console.tsx)

  • Cyberpunk-styled management panel at /admin/keys.
  • Granular permission scope multi-select (x402:quote, x402:settle, agents:read, agents:write, webhooks:*, quests:*, admin:*).
  • One-Time Plaintext Disclosure Modal: Shows the unhashed secret key exactly once upon generation with copy-to-clipboard functionality and explicit security warnings.
  • Active credentials table with live status badges, rate limit tier, request counters, and one-click key revocation.
  • Solves Admin console: display real ADMIN_API_KEY from server (not client-generated demo key) #225: Plaintext keys on /admin console are masked (osk_live_••••••••••••••••••••••••).

📦 Files Changed

Component File Path Description
Core Auth lib/auth/api-keys.ts Web Crypto key generation, SHA-256 hashing, timing-safe verification, rate limiting, and in-memory store
Middleware lib/auth/middleware.ts Route evaluator, scope enforcement, and closed-by-default logic
Middleware Entry middleware.ts Next.js root Edge proxy handler
Admin Endpoints app/api/admin/keys/route.ts REST API for key provisioning (POST), listing (GET), and revocation (DELETE)
Admin UI app/admin/keys/page.tsx Dedicated API Key Management dashboard
Admin Console components/admin/admin-console.tsx Masked key display (#225) and navigation links
Boot Config lib/admin-api-key.ts Web Crypto boot-time admin key validation
E2E Tests e2e/admin-keys.spec.ts Playwright E2E suite for key creation, scope selection, and secret disclosure
E2E Config playwright.config.ts Test timeout and environment variable configuration
Unit Tests __tests__/auth/api-keys.test.ts 15 acceptance tests covering key lifecycle, scopes, and rate limiting
Evidence Suite __tests__/auth/evidence.test.ts Automated verification of mandatory visual evidence items
Docs & Config .env.local.example, README.md Secrets documentation and API authentication instructions

🖼 Visual Evidence (Mandatory)

Evidence 1: 401 Response on Admin Route Without Key

Unauthenticated request to /admin returns 401 Unauthorized (Unauthorized: Admin API key required).

Screenshot (90)

Automated Verification Output:

{
  "targetRoute": "GET /admin",
  "authorization": "<none>",
  "httpStatus": 401,
  "allowed": false,
  "error": "Unauthorized: Admin API key required"
}

Evidence 2: 200 Response on Admin Route With Valid Key

Same /admin route successfully loading with valid ADMIN_API_KEY (Bearer osk_... or query param).

Screenshot (91)

Automated Verification Output:

{
  "targetRoute": "GET /admin",
  "authorization": "Bearer osk_admin_live...",
  "httpStatus": 200,
  "allowed": true,
  "tier": "admin",
  "scopes": ["*"],
  "isAdmin": true
}

Evidence 3: Storage Showing SHA-256 Hash (Never Plaintext Secret)

Database/Store state demonstrating that keys are stored strictly as SHA-256 hashes and public listings only return truncated prefixes.

Screenshot (93) Screenshot (102)

Automated Verification Output:

{
  "id": "key_c7c6d6836e4a5dc2",
  "name": "production-payment-relay",
  "keyPrefix": "osk_live_40e1b...",
  "hashedKey": "03b1694a3cb5a955665883482d06d95e6af7bf49769dfdf7aa13f2d16ce33cf6",
  "scopes": ["x402:quote", "x402:settle"],
  "tier": "pro"
}

Evidence 4: Admin Key Management Dashboard (/admin/keys)

UI screenshot demonstrating key creation modal, scope selectors, and the one-time secret display banner.

Screenshot (96) Screenshot (97) Screenshot (98) Screenshot (99) Screenshot (100)

🧪 Verification & Test Results

1. Playwright End-to-End Tests (npx playwright test)

  • Status: 13 / 13 Passed (across all 5 test files)
    • e2e/admin-keys.spec.ts (API key creation, scope selection, one-time secret modal, copy & dismiss, key listing)
    • e2e/admin-passport.spec.ts (ZK trust layer, proof generation, on-chain verification simulation)
    • e2e/agent-wallet.spec.ts (Agent canvas selection and wallet modal workflow)
    • e2e/onboarding-modal.spec.ts (First-visit tour navigation, multi-step panels, persistence)
    • e2e/sidebar-tabs.spec.ts (Sidebar navigation, tab switching, localStorage persistence)

2. Unit & Integration Tests (npm test)

  • Status: 97 test files passed (100%), 640 / 640 tests passed
    • __tests__/auth/api-keys.test.ts (15/15 security acceptance tests)
    • __tests__/auth/evidence.test.ts (3/3 visual evidence verification tests)

3. Static Analysis & Code Quality

  • TypeScript (npx tsc --noEmit): Clean (0 errors).
  • ESLint (npm run lint): Clean (0 errors).
  • Prettier (npx prettier --check): All files formatted according to repository code style.
  • Production Build (npm run build): Compiled successfully with static pages and Edge proxy middleware.

🔒 Security Review Checklist

  • Closed-by-default routing enforced in Edge Middleware.
  • Web Crypto CSPRNG used for key generation.
  • Constant-time equality comparison (timingSafeEqual) used for hash verification.
  • Plaintext secrets never persisted or returned after initial creation.
  • Revoked keys cannot be revived or reused.
  • Four-tier sliding-window rate limiting actively protects against DDoS/brute force.
  • No node:* modules imported in Edge middleware bundles.
  • Plaintext keys masked on /admin UI to resolve issue Admin console: display real ADMIN_API_KEY from server (not client-generated demo key) #225.

Closes #39

Comment thread lib/auth/api-keys.ts Outdated
Comment thread lib/auth/api-keys.ts Outdated
Comment thread lib/auth/middleware.ts Outdated
Comment thread lib/auth/middleware.ts Outdated
Comment thread app/api/admin/keys/route.ts
Comment thread lib/auth/middleware.ts
@Unclebaffa
Unclebaffa force-pushed the feat/api-key-management branch 2 times, most recently from b5971e4 to 8fe3cf3 Compare August 21, 2026 17:41
Comment thread lib/auth/middleware.ts Outdated
Comment thread lib/auth/api-keys.ts Outdated
Comment thread lib/auth/api-keys.ts Outdated
Comment thread lib/auth/middleware.ts
@Unclebaffa
Unclebaffa force-pushed the feat/api-key-management branch from 8fe3cf3 to b26377f Compare August 21, 2026 18:12
Comment thread lib/auth/storage.ts Outdated
Comment thread lib/auth/api-keys.ts Outdated
Comment thread lib/auth/middleware.ts Outdated
Unclebaffa added 2 commits August 21, 2026 19:28
…surfacing, and timer flush

- Item 1 (XFF spoofing): add TRUSTED_PROXY_COUNT env var; when set, pick
  ips[length - 1 - trustedProxyCount] instead of walking the private-filter
  list, eliminating forged public-hop injection attacks on known infra
- Item 2 (silent write failure): replace outer catch swallow in
  writePersistedKeys with console.error so EROFS / permission errors surface
  in application logs (inner rename-fallback preserved for atomicity)
- Item 3 (debounced timer drops counters): reduce default window 1000ms ->
  200ms; add FLUSH_USAGE_SYNC=true escape hatch for synchronous flush on
  every authenticated request for accurate usage accounting
- Item 4 (IPv6 casing + port suffixes): add normalizeIp() helper that
  lowercases and strips bracket+port ([::1]:80) and IPv4+port (1.2.3.4:443)
  before all private-range prefix checks; getClientIp normalises returned IPs
- Tests: expand getClientIp suite with uppercase FE80::1, [::1]:80,
  127.0.0.1:8080, and TRUSTED_PROXY_COUNT=2 cases (645/645 pass)
- Docs: document TRUSTED_PROXY_COUNT and FLUSH_USAGE_SYNC in .env.local.example
@gitar-bot

gitar-bot Bot commented Aug 21, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 13 resolved / 13 findings

Implements a closed-by-default API key authentication, scoped service keys, and edge rate limiting architecture, addressing 13 security and storage findings. All 640 unit tests and end-to-end suites passed successfully.

✅ 13 resolved
Bug: Scoped API keys stored only in per-runtime in-memory Map

📄 lib/auth/api-keys.ts:71-85 📄 lib/auth/api-keys.ts:249-250 📄 lib/auth/api-keys.ts:356-364 📄 middleware.ts:4-9 📄 app/api/admin/keys/route.ts:41-55
Keys minted via POST /api/admin/keys are written to a globalThis Map in lib/auth/api-keys.ts (getKeyStore). But middleware.ts runs in the Edge runtime, while route handlers run in the Node.js server, and serverless instances are ephemeral and not shared. verifyApiKey in the middleware therefore reads a different (empty/cold-start) Map than the one the admin route wrote to, so every scoped osk_live_... key fails verification in production, and keys are also lost on restart/scale-out. Only the env-based ADMIN_API_KEY works because it is read from process.env in both runtimes. Back the store with a shared persistent layer (DB/Redis/KV) that both the middleware and route handlers query, or verify keys via an internal API call rather than shared process memory.

Security: hashKey fallback is a weak 64-bit non-cryptographic hash

📄 lib/auth/api-keys.ts:124-138
When globalThis.crypto.subtle is unavailable, hashKey falls back to a hand-rolled mixing loop that is not SHA-256 and yields only 64 bits of output, despite the comment and PR claiming SHA-256 zero-knowledge storage (#225). If this path ever executes, stored hashedKey values are trivially brute-forceable/reversible and collisions become likely, defeating the credential-protection goal. Since Edge and Node both provide Web Crypto, prefer failing loudly (throw) when crypto.subtle is missing rather than silently degrading to an insecure hash.

Bug: PATCH rotate on revoked key returns 500 instead of 400

📄 app/api/admin/keys/route.ts:98-112 📄 app/api/admin/keys/route.ts:119-128 📄 lib/auth/api-keys.ts:307-321
rotateApiKey throws "Cannot rotate a revoked API key" for revoked keys, but the PATCH handler's catch block maps all thrown errors to HTTP 500, so a client-side/state error is reported as a server error. Detect this expected condition and return a 4xx (e.g., 409/400) with the message, consistent with the other validation responses in the handler.

Quality: Documented X-RateLimit-* headers are not emitted

📄 lib/auth/middleware.ts:378-392 📄 lib/auth/middleware.ts:538-552 📄 lib/auth/api-keys.ts:461-475
The PR and OpenApi rateLimit schema advertise X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset on responses, but authMiddleware only sets Retry-After (on 429) and X-Api-Tier. The checkTierRateLimit result already computes limit/remaining/resetTimeMs — thread those into result.headers and set them on both allowed and 429 responses so clients can honor the documented contract.

Security: API key in query param is read but never stripped from URL

📄 lib/auth/middleware.ts:20-34 📄 lib/auth/middleware.ts:20-22 📄 lib/auth/middleware.ts:491-505
The doc comment on extractApiKey states the query param is "sanitized from URL to prevent leakage in logs or history," but the code only reads ?apiKey= and returns it — it never removes it. Full secret keys therefore remain in request URLs and are captured by access logs, proxies, referrers, and browser history. Either drop query-param key support in favor of the Authorization header, or ensure the param is stripped/redacted before the request is logged or forwarded, and correct the misleading comment.

...and 8 more resolved from earlier reviews

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@sonarqubecloud

Copy link
Copy Markdown

@Unclebaffa

Copy link
Copy Markdown
Contributor Author

@leocagli Please review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

API key management — production auth for admin and public endpoints

1 participant